#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Modbus TCP PLC 模拟器
模拟工业设备（PLC）的 Modbus TCP 从站，提供温度、压力、流量数据
支持正常数据和异常数据模拟，方便测试报警功能
"""

import struct
import random
import time
import threading
import logging
from datetime import datetime
from pymodbus.server import StartTcpServer
from pymodbus.device import ModbusDeviceIdentification
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext


class PLCDataSimulator:
    """PLC 数据模拟器 - 模拟真实工业场景"""
    
    def __init__(self):
        # 数据范围定义（正常范围）
        self.temp_normal_range = (60.0, 75.0)      # 温度正常范围 60-75℃
        self.temp_warning_range = (75.0, 85.0)     # 温度警告范围 75-85℃
        self.temp_critical_range = (85.0, 110.0)   # 温度危险范围 85-110℃
        
        self.pressure_normal_range = (280.0, 350.0)    # 压力正常范围 280-350 kPa
        self.pressure_warning_range = (350.0, 450.0)   # 压力警告范围 350-450 kPa
        self.pressure_critical_range = (450.0, 600.0)  # 压力危险范围 450-600 kPa
        
        self.flow_normal_range = (40.0, 55.0)      # 流量正常范围 40-55 L/min
        self.flow_warning_range = (55.0, 75.0)     # 流量警告范围 55-75 L/min
        self.flow_critical_range = (75.0, 120.0)   # 流量危险范围 75-120 L/min
        
        # 当前数据值
        self.temperature = 68.5
        self.pressure = 320.0
        self.flow = 45.5
        
        # 数据变化速率（每次更新的变化量）
        self.temp_change_rate = 0.3
        self.pressure_change_rate = 2.0
        self.flow_change_rate = 0.8
        
        # 异常状态控制
        self.anomaly_mode = False
        self.anomaly_type = None  # 'temperature', 'pressure', 'flow', 'random'
        self.anomaly_counter = 0
        self.anomaly_duration = 0
        
        # 统计信息
        self.update_count = 0
        self.anomaly_count = 0
        self.start_time = datetime.now()
        
    def update_data(self):
        """更新数据 - 模拟真实的工业设备数据变化"""
        self.update_count += 1
        
        # 每 100 次更新有 5% 概率触发异常
        if self.update_count % 100 == 0 and random.random() < 0.05:
            self._trigger_anomaly()
        
        # 如果在异常模式中
        if self.anomaly_mode:
            self._update_anomaly_data()
        else:
            self._update_normal_data()
            
        # 确保数据在合理范围内（不能是负数或超出物理限制）
        self.temperature = max(0.0, min(150.0, self.temperature))
        self.pressure = max(0.0, min(1000.0, self.pressure))
        self.flow = max(0.0, min(200.0, self.flow))
        
    def _update_normal_data(self):
        """更新正常数据 - 在正常范围内随机波动"""
        # 温度：缓慢波动，有惯性
        temp_target = random.uniform(*self.temp_normal_range)
        temp_diff = temp_target - self.temperature
        self.temperature += temp_diff * 0.1 + random.uniform(-0.5, 0.5)
        
        # 压力：中等波动
        pressure_target = random.uniform(*self.pressure_normal_range)
        pressure_diff = pressure_target - self.pressure
        self.pressure += pressure_diff * 0.15 + random.uniform(-3.0, 3.0)
        
        # 流量：较快波动，受压力影响
        flow_base = 40.0 + (self.pressure - 280.0) / 70.0 * 10.0  # 流量与压力相关
        self.flow = flow_base + random.uniform(-2.0, 2.0)
        
    def _trigger_anomaly(self):
        """触发异常事件"""
        self.anomaly_mode = True
        self.anomaly_count += 1
        
        # 随机选择异常类型
        anomaly_types = ['temperature', 'pressure', 'flow', 'random']
        self.anomaly_type = random.choice(anomaly_types)
        
        # 异常持续时间：10-60 秒（10-60 次更新）
        self.anomaly_duration = random.randint(10, 60)
        self.anomaly_counter = 0
        
        logging.warning(f"⚠️ 异常触发！类型: {self.anomaly_type}, 持续: {self.anomaly_duration}秒")
        
    def _update_anomaly_data(self):
        """更新异常数据"""
        self.anomaly_counter += 1
        
        # 异常结束，恢复正常
        if self.anomaly_counter >= self.anomaly_duration:
            self.anomaly_mode = False
            self.anomaly_type = None
            logging.info(f"✓ 异常已恢复正常")
            return
        
        # 根据异常类型更新数据
        if self.anomaly_type == 'temperature':
            # 温度异常：逐渐升高到警告或危险区域
            if self.anomaly_counter < self.anomaly_duration * 0.3:
                # 前 30%：升到警告区域
                target = random.uniform(*self.temp_warning_range)
            else:
                # 后 70%：升到危险区域
                target = random.uniform(*self.temp_critical_range)
            self.temperature += (target - self.temperature) * 0.2
            
        elif self.anomaly_type == 'pressure':
            # 压力异常：快速升高
            if self.anomaly_counter < self.anomaly_duration * 0.5:
                target = random.uniform(*self.pressure_warning_range)
            else:
                target = random.uniform(*self.pressure_critical_range)
            self.pressure += (target - self.pressure) * 0.25
            
        elif self.anomaly_type == 'flow':
            # 流量异常：剧烈波动
            if random.random() < 0.5:
                target = random.uniform(*self.flow_warning_range)
            else:
                target = random.uniform(*self.flow_critical_range)
            self.flow += (target - self.flow) * 0.3
            
        elif self.anomaly_type == 'random':
            # 随机异常：所有参数都不稳定
            self.temperature += random.uniform(-2.0, 3.0)
            self.pressure += random.uniform(-10.0, 15.0)
            self.flow += random.uniform(-5.0, 8.0)
            
    def float_to_registers(self, value):
        """将 FLOAT32 转换为 2 个 16 位寄存器（大端字节序）"""
        # 使用 IEEE 754 标准打包浮点数为字节
        packed = struct.pack('>f', value)  # '>f' 表示大端字节序的 float
        # 解包为 2 个无符号 16 位整数
        register1, register2 = struct.unpack('>HH', packed)
        return [register1, register2]
    
    def get_register_values(self):
        """获取所有寄存器值（6个寄存器：温度2个、压力2个、流量2个）"""
        temp_regs = self.float_to_registers(self.temperature)
        pressure_regs = self.float_to_registers(self.pressure)
        flow_regs = self.float_to_registers(self.flow)
        return temp_regs + pressure_regs + flow_regs
    
    def get_status_string(self):
        """获取当前状态字符串"""
        status = "正常运行"
        if self.anomaly_mode:
            status = f"异常状态 ({self.anomaly_type})"
        
        return (f"[{datetime.now().strftime('%H:%M:%S')}] {status} | "
                f"温度: {self.temperature:.1f}℃ | "
                f"压力: {self.pressure:.1f} kPa | "
                f"流量: {self.flow:.1f} L/min")


class ModbusPLCServer:
    """Modbus TCP PLC 服务器"""
    
    def __init__(self, ip="0.0.0.0", port=502, slave_id=1):
        self.ip = ip
        self.port = port
        self.slave_id = slave_id
        
        # 创建数据模拟器
        self.simulator = PLCDataSimulator()
        
        # 创建 Modbus 数据存储
        # 输入寄存器（功能码 0x04）：地址 0-5，共 6 个寄存器
        self.datablock = ModbusSequentialDataBlock(0, [0] * 6)
        
        # 创建从站上下文
        self.slave_context = ModbusSlaveContext(
            di=ModbusSequentialDataBlock(0, [0] * 100),  # 离散输入
            co=ModbusSequentialDataBlock(0, [0] * 100),  # 线圈
            hr=ModbusSequentialDataBlock(0, [0] * 100),  # 保持寄存器
            ir=self.datablock,                           # 输入寄存器
            zero_mode=True                               # 使用 0 基地址，匹配上位机读取地址 0
        )
        
        # 创建服务器上下文（支持多个从站，这里只用一个）
        self.context = ModbusServerContext(
            slaves={self.slave_id: self.slave_context},
            single=False
        )
        
        # 设置设备标识信息
        self.identity = ModbusDeviceIdentification()
        self.identity.VendorName = 'Industrial Simulator'
        self.identity.ProductCode = 'PLC-SIM-001'
        self.identity.VendorUrl = 'https://github.com/modbus-simulator'
        self.identity.ProductName = 'Modbus TCP PLC Simulator'
        self.identity.ModelName = 'PLC Simulator v1.0'
        self.identity.MajorMinorRevision = '1.0.0'
        
        # 数据更新线程
        self.update_thread = None
        self.running = False
        
        # 配置日志
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S'
        )
        
    def update_registers(self):
        """定时更新寄存器数据"""
        while self.running:
            # 更新模拟数据
            self.simulator.update_data()
            
            # 获取寄存器值
            register_values = self.simulator.get_register_values()
            
            # 更新 Modbus 数据存储
            self.datablock.setValues(0, register_values)
            
            # 打印当前状态
            status = self.simulator.get_status_string()
            if self.simulator.anomaly_mode:
                logging.warning(status)
            else:
                logging.info(status)
            
            # 每秒更新一次（与上位机采集周期一致）
            time.sleep(1.0)
    
    def start(self):
        """启动 Modbus 服务器"""
        logging.info("=" * 80)
        logging.info("🏭 Modbus TCP PLC 模拟器启动")
        logging.info("=" * 80)
        logging.info(f"📡 监听地址: {self.ip}:{self.port}")
        logging.info(f"🔧 从站 ID: {self.slave_id}")
        logging.info("📊 寄存器映射:")
        logging.info("   - 地址 0-1: 温度 (FLOAT32, ℃)")
        logging.info("   - 地址 2-3: 压力 (FLOAT32, kPa)")
        logging.info("   - 地址 4-5: 流量 (FLOAT32, L/min)")
        logging.info("=" * 80)
        logging.info("💡 提示:")
        logging.info("   - 正常数据范围: 温度 60-75℃, 压力 280-350kPa, 流量 40-55L/min")
        logging.info("   - 警告数据范围: 温度 75-85℃, 压力 350-450kPa, 流量 55-75L/min")
        logging.info("   - 异常数据范围: 温度 85-110℃, 压力 450-600kPa, 流量 75-120L/min")
        logging.info("   - 自动触发异常: 每 100 秒有 5% 概率")
        logging.info("=" * 80)
        
        # 启动数据更新线程
        self.running = True
        self.update_thread = threading.Thread(target=self.update_registers, daemon=True)
        self.update_thread.start()
        
        # 启动 Modbus TCP 服务器（阻塞运行）
        try:
            StartTcpServer(
                context=self.context,
                identity=self.identity,
                address=(self.ip, self.port)
            )
        except KeyboardInterrupt:
            logging.info("\n⏹️ 收到停止信号，正在关闭服务器...")
            self.stop()
        except Exception as e:
            logging.error(f"❌ 服务器错误: {e}")
            self.stop()
    
    def stop(self):
        """停止服务器"""
        self.running = False
        if self.update_thread:
            self.update_thread.join(timeout=2.0)
        
        # 打印统计信息
        elapsed = (datetime.now() - self.simulator.start_time).total_seconds()
        logging.info("=" * 80)
        logging.info("📊 运行统计:")
        logging.info(f"   - 运行时长: {elapsed:.0f} 秒")
        logging.info(f"   - 数据更新次数: {self.simulator.update_count}")
        logging.info(f"   - 异常触发次数: {self.simulator.anomaly_count}")
        logging.info("=" * 80)
        logging.info("👋 服务器已停止")


def main():
    """主函数"""
    # 创建并启动 PLC 模拟器
    # IP: 0.0.0.0 表示监听所有网卡
    # Port: 502 是 Modbus TCP 标准端口
    # Slave ID: 1 是默认从站地址
    server = ModbusPLCServer(ip="0.0.0.0", port=502, slave_id=1)
    server.start()


if __name__ == "__main__":
    main()

